Design ATM

Ashish

Ashish Pratap Singh

medium

In this chapter, we will explore the low-level design of ATM in detail.

Lets start by clarifying the requirements:

1. Clarifying Requirements

Before starting the design, it's important to ask thoughtful questions to uncover hidden assumptions, clarify ambiguities, and define the system's scope more precisely.

Here is an example of how a discussion between the candidate and the interviewer might unfold:

After gathering the details, we can summarize the key system requirements.

1.1 Functional Requirements

  • Authenticate users using ATM card and PIN
  • Cash Withdrawal: Dispense requested amount if user has sufficient balance and the ATM has enough cash.
  • Cash Deposit: Allow users to deposit money, which should be immediately reflected in their account balance.
  • Balance Inquiry: Display the current account balance to the user.
  • Maintain internal cash inventory by denomination.
  • Dispense cash using the highest denominations available, whenever possible.
  • Display appropriate messages for insufficient balance or insufficient cash in the ATM.
  • Automatically eject the card after each transaction

1.2 Non-Functional Requirements

  • Modularity: The system should follow object-oriented principles with a clear separation of concerns.
  • Extensibility: The design should be easy to extend to support future enhancements.
  • Maintainability: Code should be well-structured, easy to test, and maintainable.
  • Atomicity: Each transaction must be atomic. For example, a withdrawal involves multiple steps (verify balance, debit account, dispense cash). If any step fails, the entire transaction should be rolled back to ensure consistency.

After the requirements are clear, lets identify the core entities/objects we will have in our system.

2. Identifying Core Entities

Core entities are the fundamental building blocks of our system. We identify them by analyzing the functional requirements and highlighting the key nouns and responsibilities that naturally map to object-oriented abstractions such as classes, enums, or interfaces.

Let’s walk through the functional requirements and extract the relevant entities:

1. The ATM authenticates users via card and PIN.

This implies the need for two entities: Card and Account. The Card encapsulates the card number and PIN, and is linked to an Account that contains user balance and account details.

2. The ATM interacts with a central banking system to process transactions.

This suggests the need for a BankService entity to simulate backend operations. It will expose methods to authenticate cards, get account balance, debit/credit funds, etc.

3. The ATM manages a cash inventory and dispenses cash using available denominations.

This indicates an ATMMachine entity that holds the cash and manages internal state. It should contain a NoteDispenser component, which handles the dispensing logic.

These core entities define the key abstractions of the ATM system and will guide the structure of our object-oriented design and class diagrams.

3. Class Design

3.1 Class Definitions

Enums

OperationType

An enumeration that defines the types of transactions a user can select (e.g., CHECK_BALANCE, WITHDRAW_CASH).

OperationType

This improves type safety and code readability when handling user menu selections.

Data Classes

Card

A simple data class representing a user's debit card.

Card

It holds the card number and PIN, acting as a data transfer object.

Account

Represents a user's bank account.

Account

It stores the account number and the current balance. It includes synchronized methods (deposit, withdraw) to ensure thread-safe balance updates.

Core Classes

ATMSystem

The central class of the system.

ATMSystem

It acts as a Facade and Singleton, providing a single entry point for all user interactions. It manages the ATM's state and coordinates with all other components.

BankService

A mock service that simulates the connection to a bank's backend.

BankService

It is responsible for authenticating cards and processing transactions by linking cards to accounts and executing deposits or withdrawals.

3.2 Class Relationships

The relationships between classes define the system's structure and data flow.

Composition

This "has-a" relationship implies ownership, where an object is composed of other objects.

  • ATM has a BankService: The ATM owns an instance of the bank service to process transactions.
  • CashDispenser has a DispenseChain: It holds the head of the note dispenser chain.
  • NoteDispenser has a DispenseChain: Each link in the chain holds a reference (nextChain) to the next link.

Association

This is a weaker "uses-a" relationship where objects are related but don't have strong ownership.

  • ATM is associated with an ATMState: The currentState of the ATM determines its behavior. This relationship is dynamic and changes as the user progresses through a transaction.
  • ATM is associated with a Card: The currentCard field holds the card being used for the current session.
  • BankService maintains an association between a Card and an Account. This mapping is crucial for retrieving account details based on the inserted card.

Implementation/Inheritance

This "is-a" relationship defines a type hierarchy.

  • IdleState, HasCardState, and AuthenticatedState implement the ATMState interface.
  • NoteDispenser implements the DispenseChain interface.
  • NoteDispenser100, NoteDispenser50, and NoteDispenser20 extend the abstract NoteDispenser class.

3.3 Key Design Patterns

Several design patterns are employed to create a robust, maintainable, and extensible system.

State Pattern

ATMState

The ATM's session management is built using the State pattern. The ATM class delegates its behavior to a currentState object (an implementation of ATMState). When a user performs an action (e.g., inserting a card), the method call is forwarded to the current state object. The state objects (IdleState, HasCardState, etc.) handle the logic and are responsible for transitioning the ATM to the next state. This keeps the ATM class clean and makes it easy to add or modify states without changing core logic.

Chain of Responsibility Pattern

This pattern is used to process cash withdrawal requests. The NoteDispenser objects are linked in a chain, from the highest denomination to the lowest. When a withdrawal is requested, the amount is passed to the head of the chain (NoteDispenser100). Each dispenser handles the amount with its own denomination and passes the remaining amount to the next dispenser in the chain. This decouples the withdrawal logic and makes it easy to add new note denominations by simply adding a new link to the chain.

Facade Pattern

The ATM class acts as a Facade. It provides a simple, high-level interface (insertCard(), enterPin(), selectOperation()) to the client (the ATMDemo class). This facade hides the complex internal workings of the state machine, the bank service interactions, and the cash dispensing subsystem, simplifying the client's interaction with the system.

Singleton Pattern

The ATM class is implemented as a Singleton to ensure that only one instance of the ATM controller exists throughout the application's lifecycle. This is logical, as the software is designed to operate a single physical machine. It's achieved through a private constructor and a static getInstance() method.

3.4 Full Class Diagram

ATM Class Diagram

4. Implementation

4.1 OperationType Enum

1class OperationType(Enum):
2    CHECK_BALANCE = "CHECK_BALANCE"
3    WITHDRAW_CASH = "WITHDRAW_CASH"
4    DEPOSIT_CASH = "DEPOSIT_CASH"

Enumerates the user-facing operations supported by the ATM interface. This provides clean routing for actions based on user selection.

4.2 Card Class

Models a physical debit card, uniquely identified by a card number and protected by a PIN for authentication.

1class Card:
2    def __init__(self, card_number: str, pin: str):
3        self._card_number = card_number
4        self._pin = pin
5    
6    def get_card_number(self) -> str:
7        return self._card_number
8    
9    def get_pin(self) -> str:
10        return self._pin

4.3 Account Class

Represents a bank account linked to one or more cards.

1class Account:
2    def __init__(self, account_number: str, balance: float):
3        self._account_number = account_number
4        self._balance = balance
5        self._cards: Dict[str, Card] = {}
6        self._lock = threading.Lock()
7    
8    def get_account_number(self) -> str:
9        return self._account_number
10    
11    def get_balance(self) -> float:
12        return self._balance
13    
14    def get_cards(self) -> Dict[str, Card]:
15        return self._cards
16    
17    def deposit(self, amount: float):
18        with self._lock:
19            self._balance += amount
20    
21    def withdraw(self, amount: float) -> bool:
22        with self._lock:
23            if self._balance >= amount:
24                self._balance -= amount
25                return True
26            return False

Synchronization ensures safe updates to balance during concurrent transactions.

4.4 BankService Class

1class BankService:
2    def __init__(self):
3        self._accounts: Dict[str, Account] = {}
4        self._cards: Dict[str, Card] = {}
5        self._card_account_map: Dict[Card, Account] = {}
6        
7        # Create sample accounts and cards
8        account1 = self.create_account("1234567890", 1000.0)
9        card1 = self.create_card("1234-5678-9012-3456", "1234")
10        self.link_card_to_account(card1, account1)
11        
12        account2 = self.create_account("9876543210", 500.0)
13        card2 = self.create_card("9876-5432-1098-7654", "4321")
14        self.link_card_to_account(card2, account2)
15    
16    def create_account(self, account_number: str, initial_balance: float) -> Account:
17        account = Account(account_number, initial_balance)
18        self._accounts[account_number] = account
19        return account
20    
21    def create_card(self, card_number: str, pin: str) -> Card:
22        card = Card(card_number, pin)
23        self._cards[card_number] = card
24        return card
25    
26    def authenticate(self, card: Card, pin: str) -> bool:
27        return card.get_pin() == pin
28    
29    def authenticate_card(self, card_number: str) -> Optional[Card]:
30        return self._cards.get(card_number)
31    
32    def get_balance(self, card: Card) -> float:
33        return self._card_account_map[card].get_balance()
34    
35    def withdraw_money(self, card: Card, amount: float):
36        self._card_account_map[card].withdraw(amount)
37    
38    def deposit_money(self, card: Card, amount: float):
39        self._card_account_map[card].deposit(amount)
40    
41    def link_card_to_account(self, card: Card, account: Account):
42        account.get_cards()[card.get_card_number()] = card
43        self._card_account_map[card] = account

A mock back-end service that handles:

  • Card/account creation
  • Authentication
  • Balance inquiry
  • Money transfers

It maintains mappings between cards and accounts and enforces account integrity.

4.5 Cash Dispensing Subsystem (Chain of Responsibility Pattern)

Dispensing a specific cash amount requires breaking it down into available note denominations (e.g., $100, $50, $20). The Chain of Responsibility pattern is a perfect fit for this, creating a flexible and extensible system for dispensing notes.

DispenseChain

1class DispenseChain(ABC):
2    @abstractmethod
3    def set_next_chain(self, next_chain: 'DispenseChain'):
4        pass
5    
6    @abstractmethod
7    def dispense(self, amount: int):
8        pass
9    
10    @abstractmethod
11    def can_dispense(self, amount: int) -> bool:
12        pass

NoteDispenser

1class NoteDispenser(DispenseChain):
2    def __init__(self, note_value: int, num_notes: int):
3        self._note_value = note_value
4        self._num_notes = num_notes
5        self._next_chain: Optional[DispenseChain] = None
6        self._lock = threading.Lock()
7    
8    def set_next_chain(self, next_chain: DispenseChain):
9        self._next_chain = next_chain
10    
11    def dispense(self, amount: int):
12        with self._lock:
13            if amount >= self._note_value:
14                num_to_dispense = min(amount // self._note_value, self._num_notes)
15                remaining_amount = amount - (num_to_dispense * self._note_value)
16                
17                if num_to_dispense > 0:
18                    print(f"Dispensing {num_to_dispense} x ${self._note_value} note(s)")
19                    self._num_notes -= num_to_dispense
20                
21                if remaining_amount > 0 and self._next_chain is not None:
22                    self._next_chain.dispense(remaining_amount)
23            elif self._next_chain is not None:
24                self._next_chain.dispense(amount)
25    
26    def can_dispense(self, amount: int) -> bool:
27        with self._lock:
28            if amount < 0:
29                return False
30            if amount == 0:
31                return True
32            
33            num_to_use = min(amount // self._note_value, self._num_notes)
34            remaining_amount = amount - (num_to_use * self._note_value)
35            
36            if remaining_amount == 0:
37                return True
38            if self._next_chain is not None:
39                return self._next_chain.can_dispense(remaining_amount)
40            return False

The DispenseChain interface establishes a common contract for all links.

The NoteDispenser abstract class contains the shared logic:

  • It handles requests for its specific noteValue (e.g., $100).
  • It calculates how many of its notes to dispense.
  • It passes the remaining amount down to the nextChain.

This design is highly modular. To add a new note denomination (e.g., $10), we would simply create a new NoteDispenser10 class and insert it into the chain, with no changes to existing code.

Concrete Note Dispensers

Specialized implementations of NoteDispenser for different note denominations. These are the specific links in our chain, each responsible for a single note denomination.

1class NoteDispenser20(NoteDispenser):
2    def __init__(self, num_notes: int):
3        super().__init__(20, num_notes)
4
5class NoteDispenser50(NoteDispenser):
6    def __init__(self, num_notes: int):
7        super().__init__(50, num_notes)
8
9class NoteDispenser100(NoteDispenser):
10    def __init__(self, num_notes: int):
11        super().__init__(100, num_notes)

Each class is a simple extension of NoteDispenser, specifying its note value.

This design:

  • Allows fallback to lower denominations
  • Supports extension for additional note types

CashDispenser Facade

The CashDispenser class acts as a client for the chain, initiating the dispensing process by passing the request to the first link.

1class CashDispenser:
2    def __init__(self, chain: DispenseChain):
3        self._chain = chain
4        self._lock = threading.Lock()
5    
6    def dispense_cash(self, amount: int):
7        with self._lock:
8            self._chain.dispense(amount)
9    
10    def can_dispense_cash(self, amount: int) -> bool:
11        with self._lock:
12            if amount % 10 != 0:
13                return False
14            return self._chain.can_dispense(amount)

4.6 State Pattern: ATMState and Implementations

ATMState Interface

Defines different states of the ATM session: Idle, HasCard, and Authenticated. Each state governs what actions are allowed.

1class ATMState(ABC):
2    @abstractmethod
3    def insert_card(self, atm: 'ATM', card_number: str):
4        pass
5    
6    @abstractmethod
7    def enter_pin(self, atm: 'ATM', pin: str):
8        pass
9    
10    @abstractmethod
11    def select_operation(self, atm: 'ATM', op: OperationType, *args):
12        pass
13    
14    @abstractmethod
15    def eject_card(self, atm: 'ATM'):
16        pass

IdleState

1class IdleState(ATMState):
2    def insert_card(self, atm: 'ATM', card_number: str):
3        print("\nCard has been inserted.")
4        card = atm.get_bank_service().authenticate_card(card_number)
5        
6        if card is None:
7            self.eject_card(atm)
8        else:
9            atm.set_current_card(card)
10            atm.change_state(HasCardState())
11    
12    def enter_pin(self, atm: 'ATM', pin: str):
13        print("Error: Please insert a card first.")
14    
15    def select_operation(self, atm: 'ATM', op: OperationType, *args):
16        print("Error: Please insert a card first.")
17    
18    def eject_card(self, atm: 'ATM'):
19        print("Error: Card not found.")
20        atm.set_current_card(None)

Initial state when ATM is idle. Only card insertion is valid here. Transitions to HasCardState upon success.

HasCardState

1class HasCardState(ATMState):
2    def insert_card(self, atm: 'ATM', card_number: str):
3        print("Error: A card is already inserted. Cannot insert another card.")
4    
5    def enter_pin(self, atm: 'ATM', pin: str):
6        print("Authenticating PIN...")
7        card = atm.get_current_card()
8        is_authenticated = atm.get_bank_service().authenticate(card, pin)
9        
10        if is_authenticated:
11            print("Authentication successful.")
12            atm.change_state(AuthenticatedState())
13        else:
14            print("Authentication failed: Incorrect PIN.")
15            self.eject_card(atm)
16    
17    def select_operation(self, atm: 'ATM', op: OperationType, *args):
18        print("Error: Please enter your PIN first to select an operation.")
19    
20    def eject_card(self, atm: 'ATM'):
21        print("Card has been ejected. Thank you for using our ATM.")
22        atm.set_current_card(None)
23        atm.change_state(IdleState())

Waits for PIN input. Validates the PIN and moves to AuthenticatedState if correct, else ejects the card.

AuthenticatedState

1class AuthenticatedState(ATMState):
2    def insert_card(self, atm: 'ATM', card_number: str):
3        print("Error: A card is already inserted and a session is active.")
4    
5    def enter_pin(self, atm: 'ATM', pin: str):
6        print("Error: PIN has already been entered and authenticated.")
7    
8    def select_operation(self, atm: 'ATM', op: OperationType, *args):
9        if op == OperationType.CHECK_BALANCE:
10            atm.check_balance()
11        elif op == OperationType.WITHDRAW_CASH:
12            if len(args) == 0 or args[0] <= 0:
13                print("Error: Invalid withdrawal amount specified.")
14                return
15            
16            amount_to_withdraw = args[0]
17            account_balance = atm.get_bank_service().get_balance(atm.get_current_card())
18            
19            if amount_to_withdraw > account_balance:
20                print("Error: Insufficient balance.")
21                return
22            
23            print(f"Processing withdrawal for ${amount_to_withdraw}")
24            atm.withdraw_cash(amount_to_withdraw)
25        elif op == OperationType.DEPOSIT_CASH:
26            if len(args) == 0 or args[0] <= 0:
27                print("Error: Invalid deposit amount specified.")
28                return
29            
30            amount_to_deposit = args[0]
31            print(f"Processing deposit for ${amount_to_deposit}")
32            atm.deposit_cash(amount_to_deposit)
33        else:
34            print("Error: Invalid operation selected.")
35            return
36        
37        # End the session after one transaction
38        print("Transaction complete.")
39        self.eject_card(atm)
40    
41    def eject_card(self, atm: 'ATM'):
42        print("Ending session. Card has been ejected. Thank you for using our ATM.")
43        atm.set_current_card(None)
44        atm.change_state(IdleState())

Allows all operations. After any operation, session ends by calling ejectCard.

4.7 ATM Class (Facade)

The ATM class orchestrates all the subsystems. It uses the Singleton Pattern to ensure only one instance of the physical ATM exists and the Facade Pattern to provide a simple, unified interface to its complex internal operations.

1class ATM:
2    _instance = None
3    _lock = threading.Lock()
4    
5    def __new__(cls):
6        if cls._instance is None:
7            with cls._lock:
8                if cls._instance is None:
9                    cls._instance = super().__new__(cls)
10                    cls._instance._initialized = False
11        return cls._instance
12    
13    def __init__(self):
14        if not self._initialized:
15            self._current_state = IdleState()
16            self._bank_service = BankService()
17            self._current_card: Optional[Card] = None
18            self._transaction_counter = 0
19            
20            # Setup the dispenser chain
21            c1 = NoteDispenser100(10)  # 10 x $100 notes
22            c2 = NoteDispenser50(20)   # 20 x $50 notes
23            c3 = NoteDispenser20(30)   # 30 x $20 notes
24            c1.set_next_chain(c2)
25            c2.set_next_chain(c3)
26            self._cash_dispenser = CashDispenser(c1)
27            self._initialized = True
28    
29    @classmethod
30    def get_instance(cls):
31        return cls()
32    
33    def change_state(self, new_state: ATMState):
34        self._current_state = new_state
35    
36    def set_current_card(self, card: Optional[Card]):
37        self._current_card = card
38    
39    def insert_card(self, card_number: str):
40        self._current_state.insert_card(self, card_number)
41    
42    def enter_pin(self, pin: str):
43        self._current_state.enter_pin(self, pin)
44    
45    def select_operation(self, op: OperationType, *args):
46        self._current_state.select_operation(self, op, *args)
47    
48    def check_balance(self):
49        balance = self._bank_service.get_balance(self._current_card)
50        print(f"Your current account balance is: ${balance:.2f}")
51    
52    def withdraw_cash(self, amount: int):
53        if not self._cash_dispenser.can_dispense_cash(amount):
54            raise RuntimeError("Insufficient cash available in the ATM.")
55        
56        self._bank_service.withdraw_money(self._current_card, amount)
57        
58        try:
59            self._cash_dispenser.dispense_cash(amount)
60        except Exception as e:
61            self._bank_service.deposit_money(self._current_card, amount)  # Deposit back if dispensing fails
62            raise e
63    
64    def deposit_cash(self, amount: int):
65        self._bank_service.deposit_money(self._current_card, amount)
66    
67    def get_current_card(self) -> Optional[Card]:
68        return self._current_card
69    
70    def get_bank_service(self) -> BankService:
71        return self._bank_service
  • Delegates session behavior to current state (ATMState)
  • Manages card info, state transitions, and coordinates with BankService and CashDispenser

4.8 ATM Demo

The ATMDemo class simulates a user interacting with the ATM, demonstrating various user flows and edge cases.

1class ATMDemo:
2    @staticmethod
3    def main():
4        atm = ATM.get_instance()
5        
6        # Perform Check Balance operation
7        atm.insert_card("1234-5678-9012-3456")
8        atm.enter_pin("1234")
9        atm.select_operation(OperationType.CHECK_BALANCE)  # $1000
10        
11        # Perform Withdraw Cash operation
12        atm.insert_card("1234-5678-9012-3456")
13        atm.enter_pin("1234")
14        atm.select_operation(OperationType.WITHDRAW_CASH, 570)
15        
16        # Perform Deposit Cash operation
17        atm.insert_card("1234-5678-9012-3456")
18        atm.enter_pin("1234")
19        atm.select_operation(OperationType.DEPOSIT_CASH, 200)
20        
21        # Perform Check Balance operation
22        atm.insert_card("1234-5678-9012-3456")
23        atm.enter_pin("1234")
24        atm.select_operation(OperationType.CHECK_BALANCE)  # $630
25        
26        # Perform Withdraw Cash more than balance
27        atm.insert_card("1234-5678-9012-3456")
28        atm.enter_pin("1234")
29        atm.select_operation(OperationType.WITHDRAW_CASH, 700)  # Insufficient balance
30        
31        # Insert Incorrect PIN
32        atm.insert_card("1234-5678-9012-3456")
33        atm.enter_pin("3425")
34
35if __name__ == "__main__":
36    ATMDemo.main()

5. Run and Test

Languages
Java
C#
Python
C++
Files13
dispensechain
entities
enum
service
states
atm_demo.py
main
atm.py
atm_demo.py
Output

6. Quiz

Design ATM - Quiz

1 / 21
Multiple Choice

Which entity is primarily responsible for managing the cash inventory and dispensing cash in an ATM system?

How helpful was this article?

Comments


0/2000

No comments yet. Be the first to comment!

Copilot extension content script